package aliyun

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"
	"github.com/gogf/gf/v2/container/gvar"
	"github.com/gogf/gf/v2/crypto/gmd5"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/os/gtime"
	"github.com/gogf/gf/v2/text/gregex"
	"github.com/gogf/gf/v2/text/gstr"
	"github.com/gogf/gf/v2/util/grand"
	"github.com/google/uuid"
)

var ossInstanceCache map[string]*ossService
var ossMutex sync.Mutex

func init() {
	ossInstanceCache = make(map[string]*ossService)
	ossMutex = sync.Mutex{}
}

func getOssInstance(key string) *ossService {
	if v, ok := ossInstanceCache[key]; ok {
		return v
	}
	return nil
}

func setOssInstance(key string, v *ossService) {
	// 写加锁，避免并发
	ossMutex.Lock()
	ossInstanceCache[key] = v
	ossMutex.Unlock()
}

func getOssConfig(key string, bucket ...string) *gvar.Var {
	if bucket != nil && len(bucket) > 0 && bucket[0] != "" {
		v := g.Config().MustGet(context.Background(), fmt.Sprintf("app.aliyun.oss.%s.%s", bucket[0], key))
		if !v.IsEmpty() {
			return v
		}
		if key == "bucket" {
			// 如果没取到对应配置，则 bucket 设置为传入值
			return gvar.New(bucket[0])
		}
	}
	return g.Config().MustGet(context.Background(), fmt.Sprintf("app.aliyun.oss.%s", key))
}

// Oss 初始化，用以支持多个oss配置项
// bucket 默认为空
// 为空，则从 app.aliyun.oss 配置读取
// 不为空，则从 app.aliyun.oss.xxxxxx 配置读取
func Oss(bucket ...string) *ossService {
	/*
		benchmark
		cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
		无缓存：1000000000               0.0000533 ns/op
		有缓存：1000000000               0.0000136 ns/op
	*/
	cacheKey := "_default_"
	if bucket != nil && len(bucket) > 0 && bucket[0] != "" {
		cacheKey = bucket[0]
	}
	ins := getOssInstance(cacheKey)
	if ins != nil {
		return ins
	}

	instance := &ossService{
		endpoint:           getOssConfig("endpoint", bucket...).String(),
		bucket:             getOssConfig("bucket", bucket...).String(),
		permission:         getOssConfig("permission", bucket...).String(),
		endpointPrivate:    getOssConfig("endpointPrivate", bucket...).String(),
		endpointPublic:     getOssConfig("endpointPublic", bucket...).String(),
		endpointAccelerate: getOssConfig("endpointAccelerate", bucket...).String(),
		cdnDomain:          getOssConfig("cdnDomain", bucket...).String(),
		cdnKey:             getOssConfig("cdnKey", bucket...).String(),
		cdnExpire:          getOssConfig("cdnExpire", bucket...).Int64(),
	}

	if instance.cdnExpire <= 0 {
		instance.cdnExpire = 3600
	}

	// 配置校验
	if instance.endpoint == "" {
		panic("the oss endpoint is empty")
	}
	if instance.endpointPublic == "" {
		panic("the oss public endpoint is empty")
	}
	if instance.endpointPrivate == "" {
		panic("the oss private endpoint is empty")
	}
	if instance.bucket == "" {
		panic("the oss bucket is empty")
	}

	instance.ossClient, _ = oss.New(fmt.Sprintf("https://%s", instance.endpoint), Config.accessId, Config.accessKey)
	instance.ossPublicClient, _ = oss.New(fmt.Sprintf("https://%s", instance.endpointPublic), Config.accessId, Config.accessKey)
	instance.ossPrivateClient, _ = oss.New(fmt.Sprintf("https://%s", instance.endpointPrivate), Config.accessId, Config.accessKey)

	if instance.endpointAccelerate != "" {
		instance.ossAccelerateClient, _ = oss.New(fmt.Sprintf("https://%s", instance.endpointAccelerate), Config.accessId, Config.accessKey)
	}

	setOssInstance(cacheKey, instance)

	return instance
}

type ossService struct {
	endpoint            string
	bucket              string
	permission          string
	endpointPrivate     string
	endpointPublic      string
	endpointAccelerate  string
	cdnDomain           string
	cdnKey              string
	cdnExpire           int64
	ossClient           *oss.Client
	ossPrivateClient    *oss.Client
	ossPublicClient     *oss.Client
	ossAccelerateClient *oss.Client
}

// OssObject
// oss 统一使用一种自定义的 uri 来定位文件
// oss://endpoint/bucket/object
type OssObject struct {
	Endpoint string
	Bucket   string
	Object   string
}

func (r *OssObject) String() string {
	return fmt.Sprintf("oss://%s/%s/%s", r.Endpoint, r.Bucket, r.Object)
}

func (o *ossService) formatObjectName(object string) string {
	return strings.Trim(object, "/")
}

// bucket
// 获取 oss bucket
func (o *ossService) createBucket(ossClient ...*oss.Client) *oss.Bucket {
	if ossClient != nil && len(ossClient) > 0 {
		bucket, _ := ossClient[0].Bucket(o.bucket)
		return bucket
	}
	bucket, _ := o.ossClient.Bucket(o.bucket)
	return bucket
}

// ParseOssObject 从url（oss和http协议）中解析为OssObject对象
// 示例:
// oss://endpoint/bucket/object
// https://picwishsz.oss-cn-shenzhen.aliyuncs.com/tasks/input/test.jpg
func (o *ossService) ParseOssObject(uri string) (*OssObject, error) {
	u, err := url.ParseRequestURI(uri)
	if err != nil {
		return nil, err
	}
	uriScheme := strings.ToLower(u.Scheme)

	if uriScheme == "oss" {
		endpoint := u.Host
		paths := gstr.SplitAndTrim(u.Path, "/")
		if len(paths) < 2 {
			return nil, errors.New("invalid oss uri")
		}
		var bucketName string
		var objectName string
		switch len(paths) {
		case 2:
			bucketName = paths[0]
			objectName = paths[1]
		default:
			bucketName = paths[0]
			objectName = gstr.Join(paths[1:], "/")
		}

		info := &OssObject{
			Endpoint: endpoint,
			Bucket:   bucketName,
			Object:   o.formatObjectName(objectName),
		}
		return info, nil
	} else if uriScheme == "http" || uriScheme == "https" {
		matches, err := gregex.MatchString(`([\w+\-]+).(oss-[\w\-]+.aliyuncs.com)`, uri)
		if err != nil {
			return nil, err
		}
		if len(matches) != 3 {
			return nil, errors.New("invalid http uri")
		}
		info := &OssObject{
			Endpoint: matches[2],
			Bucket:   matches[1],
			Object:   o.formatObjectName(u.Path),
		}
		return info, nil
	}

	return nil, errors.New("unsupported uri scheme")
}

func (o *ossService) NewOssObject(object string) *OssObject {
	ossObj, err := o.ParseOssObject(object)
	if err != nil {
		return &OssObject{
			Endpoint: o.endpoint,
			Bucket:   o.bucket,
			Object:   o.formatObjectName(object),
		}
	}
	return ossObj
}

// CopyObject
// 复制object
func (o *ossService) CopyObject(srcObject, destObject string, options ...oss.Option) error {
	bucket := o.createBucket()
	_, err := bucket.CopyObject(srcObject, destObject, options...)
	return err
}

// CopyObjectTo
// 参考：https://help.aliyun.com/document_detail/88645.html
// 注意：不支持跨地域拷贝。例如不能将华东1（杭州）地域存储空间中的文件拷贝到华北1（青岛）地域。
func (o *ossService) CopyObjectTo(srcObjectKey, destBucketName, destObjectKey string, options ...oss.Option) error {
	bucket := o.createBucket()
	_, err := bucket.CopyObjectTo(destBucketName, destObjectKey, srcObjectKey, options...)
	return err
}

// CopyObjectFrom
// 参考：https://help.aliyun.com/document_detail/88645.html
// 注意：不支持跨地域拷贝。例如不能将华东1（杭州）地域存储空间中的文件拷贝到华北1（青岛）地域。
func (o *ossService) CopyObjectFrom(srcBucketName, srcObjectKey, destObjectKey string, options ...oss.Option) error {
	bucket := o.createBucket()
	_, err := bucket.CopyObjectFrom(srcBucketName, srcObjectKey, destObjectKey, options...)
	return err
}

func (o *ossService) RenameObject(srcObject string, destObject string) error {
	err := o.CopyObject(srcObject, destObject)
	if err != nil {
		return err
	}
	err = o.DeleteObject(srcObject)
	return err
}

// DeleteObject
// 删除 object
func (o *ossService) DeleteObject(object string, options ...oss.Option) error {
	bucket := o.createBucket()
	return bucket.DeleteObject(object, options...)
}

// DeleteObjects
// 以第一个元素的 bucket 为准
func (o *ossService) DeleteObjects(objects []string, options ...oss.Option) ([]string, error) {
	if len(objects) == 0 {
		return nil, nil
	}
	bucket := o.createBucket()
	deleteObjects, err := bucket.DeleteObjects(objects, options...)
	if err != nil {
		return nil, err
	}
	return deleteObjects.DeletedObjects, nil
}

// PutObject
// 将一个 reader 推入 oss
func (o *ossService) PutObject(object string, reader io.Reader, options ...oss.Option) error {
	bucket := o.createBucket()
	var err error
	for i := 0; i < 3; i++ {
		err = bucket.PutObject(object, reader, options...)
		if err == nil {
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return err
}

// PutFile
// 将一个 file 推入 oss
func (o *ossService) PutFile(object string, filePath string, options ...oss.Option) error {
	bucket := o.createBucket()
	var err error
	for i := 0; i < 3; i++ {
		err = bucket.PutObjectFromFile(object, filePath, options...)
		if err == nil {
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return err
}

// Download
// 下载 object 到本地文件
func (o *ossService) Download(object string, outputPath string, options ...oss.Option) error {
	bucket := o.createBucket()
	// 文件不存在则创建
	dirName := gfile.Dir(outputPath)
	if !gfile.Exists(dirName) {
		err := gfile.Mkdir(dirName)
		if err != nil {
			return err
		}
	}

	var err error
	for i := 0; i < 3; i++ {
		err = bucket.GetObjectToFile(object, outputPath, options...)
		if err == nil {
			return nil
		}
		time.Sleep(1 * time.Second)
	}
	return err
}

func (o *ossService) ListObjects(prefix string, maxKeys int, callback func([]oss.ObjectProperties, error)) {
	bucket := o.createBucket()
	continueToken := ""
	for {
		res, err := bucket.ListObjectsV2(oss.Prefix(prefix), oss.MaxKeys(maxKeys), oss.ContinuationToken(continueToken))
		callback(res.Objects, err)
		if res.IsTruncated {
			continueToken = res.NextContinuationToken
		} else {
			break
		}
	}
}

func (o *ossService) getSignedObjectUrl(client *oss.Client, object string, expiredIn int64, options ...oss.Option) string {
	if client == nil {
		return ""
	}
	object = gstr.Trim(object, "/")

	bucket := o.createBucket(client)
	signURL, err := bucket.SignURL(object, oss.HTTPGet, expiredIn, options...)
	if err != nil {
		return ""
	}
	// %2f to /
	return gstr.Replace(signURL, "%2F", "/")
}

// GetSignedObjectUrl
// 获取 object 签名地址
func (o *ossService) GetSignedObjectUrl(object string, expiredIn int64, options ...oss.Option) string {
	return o.getSignedObjectUrl(o.ossClient, object, expiredIn, options...)
}

// GetAccelerateSignedObjectUrl 获取 object 加速访问地址
func (o *ossService) GetAccelerateSignedObjectUrl(object string, expiredIn int64, options ...oss.Option) string {
	return o.getSignedObjectUrl(o.ossAccelerateClient, object, expiredIn, options...)
}

// GetPrivateSignedObjectUrl
// 获取 object 签名私有地址
func (o *ossService) GetPrivateSignedObjectUrl(object string, expiredIn int64, options ...oss.Option) string {
	return o.getSignedObjectUrl(o.ossPrivateClient, object, expiredIn, options...)
}

// GetPublicSignedObjectUrl
// 获取 object 签名公有地址
func (o *ossService) GetPublicSignedObjectUrl(object string, expiredIn int64, options ...oss.Option) string {
	return o.getSignedObjectUrl(o.ossPublicClient, object, expiredIn, options...)
}

func (o *ossService) GetOssSchemeUri(bucket, object string) string {
	return fmt.Sprintf("oss://%s/%s/%s", o.endpoint, bucket, o.formatObjectName(object))
}

func (o *ossService) GetPublicOssSchemeUri(bucket, object string) string {
	return fmt.Sprintf("oss://%s/%s/%s", o.endpointPublic, bucket, o.formatObjectName(object))
}

func (o *ossService) GetPrivateOssSchemeUri(bucket, object string) string {
	return fmt.Sprintf("oss://%s/%s/%s", o.endpointPrivate, bucket, o.formatObjectName(object))
}

// GetLiveChannelUrl 获取直播推流地址，参见：https://help.aliyun.com/document_detail/44297.html
func (o *ossService) GetLiveChannelUrl() (publishUrl string, signedPlayUrl string, err error) {
	bucket := o.createBucket()
	channelName := gstr.SubStr(uuid.New().String(), 0, 2)
	playlistName := fmt.Sprintf("%s.m3u8", uuid.New().String())

	_, err = bucket.CreateLiveChannel(channelName, oss.LiveChannelConfiguration{
		Target: oss.LiveChannelTarget{
			Type:         "HLS",
			PlaylistName: playlistName,
		},
	})
	if err != nil {
		return "", "", err
	}

	options := oss.Process("hls/sign")

	object := fmt.Sprintf("%s/%s", channelName, playlistName)
	signedPlayUrl = o.GetPublicSignedObjectUrl(object, 3600, options)

	signedPublishUrl, err := bucket.SignRtmpURL(channelName, playlistName, 3600)
	if err != nil {
		return "", "", err
	}

	//startTime := gtime.NewFromTimeStamp(1645180351386).Time
	//endTime := gtime.NewFromTimeStamp(1645180412450).Time
	//bucket.PostVodPlaylist("4e4df782-7e96-4369-95eb-3b8ebce3fc01", "feda5302-a230-4602-9dc0-08e92c2207af.m3u8", startTime, endTime)
	return signedPublishUrl, signedPlayUrl, nil
	/*
		ffmpeg -i 1.flv -c copy -f flv  rtmp://wxharrytestsz.oss-cn-shenzhen.aliyuncs.com/live/bd011302-663d-460f-a1f1-fea5eb781043?Expires=1645183474&OSSAccessKeyId=LTAI5tGjJnh66c1txANiRBQN&Signature=m4RPs0tWd9PR6cBSLE1eotKpYL8%3D&playlistName=2fdb9a0a-2132-4b8d-b5e1-f654b730dfcc.m3u8

		m3u8签名问题
		https://help.aliyun.com/document_detail/181558.html
	*/
}

func (o *ossService) SetObjectHeader(object string, headers map[string]interface{}) error {
	return o.SetObjectHeaderAndMeta(object, headers, nil)
}

// SetObjectMeta
// 设置object的元数据，oss会自动加上 x-oss-meta- 前缀
// 比如设置 { "resource": "0000086d-7f10-3269-b891-a184ba95ecd8" }
// 那么最终会形成 x-oss-meta-resource: 0000086d-7f10-3269-b891-a184ba95ecd8
func (o *ossService) SetObjectMeta(object string, metas map[string]string) error {
	return o.SetObjectHeaderAndMeta(object, nil, metas)
}
func (o *ossService) SetObjectHeaderAndMeta(object string, headers map[string]interface{}, metas map[string]string) error {
	var options []oss.Option
	if headers != nil && len(headers) > 0 {
		for k, v := range headers {
			options = append(options, oss.SetHeader(k, v))
		}
	}

	if metas != nil && len(metas) > 0 {
		for k, v := range metas {
			options = append(options, oss.Meta(k, v))
		}
	}

	if options != nil && len(options) > 0 {
		bucket := o.createBucket()
		return bucket.SetObjectMeta(object, options...)
	}
	return nil
}

func (o *ossService) SetObjectTag(object string, tags map[string]string) error {
	var options []oss.Tag
	if tags != nil && len(tags) > 0 {
		for k, v := range tags {
			options = append(options, oss.Tag{Key: k, Value: v})
		}
	}
	if len(options) > 0 {
		tagging := oss.Tagging{
			Tags: options,
		}
		bucket := o.createBucket()
		// 设置对象标签。
		return bucket.PutObjectTagging(object, tagging)
	}
	return nil
}

func (o *ossService) GetObjectTags(object string, options ...oss.Option) map[string]string {
	bucket := o.createBucket()
	tagging, err := bucket.GetObjectTagging(object, options...)
	if err != nil {
		return nil
	}
	m := map[string]string{}
	for _, tag := range tagging.Tags {
		m[tag.Key] = tag.Value
	}
	return m
}

// GetCdnUrl 获取object的CDN访问地址
// 示例
// aliyun.Oss().GetCdnUrl("earth-1920x1080-moon-planet-galaxy-4k-17034.jpg")
// 如果该bucket为public，则不会进行签名，否则进行签名处理  "?x-oss-process=style/720p",
func (o *ossService) GetCdnUrl(object string, query ...string) string {
	path := "/" + o.formatObjectName(object)
	queries := gstr.Join(query, "&")
	uri := ""
	symbol := "?"
	if o.permission == "public" || gstr.HasSuffix(o.bucket, "pub") {
		uri = fmt.Sprintf("https://%s%s", o.cdnDomain, path)
	} else {
		// 参考 https://help.aliyun.com/document_detail/85113.html
		ts := gtime.Now().Timestamp() + o.cdnExpire
		rand := grand.Digits(6)
		uid := grand.Digits(6)
		raw := fmt.Sprintf("%s-%d-%s-%s-%s", path, ts, rand, uid, o.cdnKey)
		hash := gmd5.MustEncrypt(raw)
		uri = fmt.Sprintf("https://%s%s?auth_key=%d-%s-%s-%s", o.cdnDomain, path, ts, rand, uid, hash)
		symbol = "&"
	}

	if queries != "" {
		uri = uri + symbol + queries
	}
	return uri
}
